Repository navigation
feat(catalog): categories and products - #4
Merged
Merged
Conversation
Category and Product are table-shaped classes documented with the jakarta.persistence annotations, and CategoryDto and ProductDto are the business shapes the services expose. The unit price is a BigDecimal on both sides, never a double, because money must not suffer binary rounding. The two shapes stay separate classes even though their fields match today.
The RowMappers read one row into an entity, using rs.getBigDecimal for the NUMERIC unit price so the exact decimal value survives the round trip. The entity mappers convert between entity and dto in both directions. Neither queries the database or holds any business rule.
The DAOs write the SQL of the catalog with JdbcTemplate through AbstractDao: paginated reads ordered by id with LIMIT and OFFSET, INSERT ... RETURNING id, updates and deletes. ProductDao chooses between two statements depending on whether a category filter is given, which keeps every query plain and index-friendly, and counts products per category for the delete rule. They work in entity types only and have no stereotype annotation.
The services own the business rules: names are trimmed and bounded, category names are unique, a category that still has products cannot be deleted, a product must reference an existing category, and prices are non-negative decimals with at most two digits after the point, normalised to scale two. Races are handled without an exception translator: a DuplicateKeyException or DataIntegrityViolationException raised by a concurrent change is turned into the same business refusal as the pre-check, so it never leaks as a 500. The service returns dto types and pages through PageDto.
DaoConfig and ServiceConfig declare the beans typed by interface, and ServiceConfig imports DaoConfig so importing it alone is enough. The service beans are proxied for @transactional through their interfaces. The catalog module needs no configuration of its own beyond these two classes.
Requests carry Bean Validation constraints: bounded names, and for the unit price a non-null decimal that is at least 0.00 with at most ten integer digits and two decimals, so a malformed price is a 400 before it reaches the service. Converters map to and from the core dto classes and build the PageResponse. Payload classes stay distinct from core dto classes.
Both controllers follow the README table with declarative @PreAuthorize("hasPermission('CATEGORY'|'PRODUCT', 'READ'|'WRITE')"), only call their service and return ApiResponse for every answer. The list endpoints validate page and size, and the product list accepts an optional categoryId filter. Creations return 201, and there is no business logic in either controller.
The catalog ServiceConfig is added to the WebMvcConfig import list, by fully qualified name because every core module has a class with that simple name. Importing the ServiceConfig alone is enough since it imports its own DaoConfig.
…odule The catalog DAO tests run against a real PostgreSQL, so the module declares the Testcontainers JUnit and PostgreSQL artifacts and the common test-jar holding the shared singleton container, all with test scope, exactly like the auth module. They stay out of the parent pom because only modules with DAO tests need them.
Each DAO test class loads its own dedicated fixture, truncating only the tables it touches. They check CRUD, page ordering and offsets, the optional category filter, counts per category, a duplicate category name, and the constraints owned by the schema: the foreign key on the category, the CHECK on a negative price and the exact NUMERIC round trip of 12.50. A delete refused by a foreign key surfaces as DataIntegrityViolationException, which is what the service relies on for its race handling.
RowMapper tests use a mocked ResultSet, including the BigDecimal unit price, and the entity mappers are checked with hand-built objects and no mocks. They pin the mapping contracts without needing a database.
With mocked DAOs the tests cover the page bounds, the 404 cases, name trimming and length limits, duplicate names including the DuplicateKeyException race, and the refusal to delete a category that still has products, both from the count and from the foreign key race. For products they cover an unknown category, every price rule (negative, too many decimals, too large, normalised to two decimals) and the foreign key race on delete.
The real security configuration and a real signed JWT are used with mocked services. Every endpoint is checked for status and JSON shape, validation errors per field including each price case, page and size violations, a page beyond the last that keeps content as an empty array, and the categoryId filter passed to the service. The 404 and 422 mappings, malformed JSON, 415 and 405 are covered too.
A parameterized test asserts, for every endpoint, that no token gives 401, that a token holding every other permission or none gives 403, that a blacklisted token gives 401, and that a token holding only the required permission succeeds. It proves each route is guarded by its own CATEGORY or PRODUCT permission.
Real services and a real database prove the CRUD of categories and products, the category filter, the refusal to delete a category with products followed by success once the product is gone, an unknown category giving 404 with nothing stored, duplicate names giving 422 and the exact stored price. Users holding only CATEGORY:WRITE or only PRODUCT:WRITE, a read-only role and a user without permission are checked for 403. Test rows are named with a random suffix and removed afterwards, so the shared database stays clean.
The five catalog tasks are checked: the entities, dto classes, RowMappers, mappers, DAOs, services and configuration of core/catalog, the category deletion rule, the controllers and converters in ws, the import in WebMvcConfig, and the tests at all five layers with a dedicated fixture per DAO test class.
…tegory cleanPrice called setScale before comparing with the maximum, so a value such as 1E+999999999 made BigDecimal build an enormous number before being refused. The bound is now checked first, on the raw value. create and update also checked that the category exists and then wrote, so a category deleted in between made the foreign key raise a DataIntegrityViolationException that became a 500. Both now translate it into the same ResourceNotFoundException as the pre-check. Three unit tests pin these cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch
feature/catalog
Task checklist
core/catalog:Category,Productentities, dto classes, RowMappers, mappers, DAOs, services,DaoConfig/ServiceConfigBusinessRuleException-> 422)ws:CategoryController,ProductController,CategoryConverter,ProductConverterWebMvcConfigimportscore/catalog's configCommit summary
Limits documented: HTTP rejects prices such as 1.500 (at most two decimals), a product update racing a category delete can 500 on the FK only in the narrow update window handled at write time.
Test plan
mvn clean verifyis green on the whole reactor